#include <iostream>
#include <functional>
#include <string>
#include <unordered_set>
#include <strings.h>
#include <cstring>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <unistd.h>

#include "log.hpp"

using namespace std;

logger lg;

using handler_t = function<string(const string &)>;

struct sockaddr_in_hash {
    const size_t operator()(const struct sockaddr_in &addr) const {
        return (size_t)addr.sin_addr.s_addr;
    }
};

struct sockaddr_in_equal  {
    bool operator()(const struct sockaddr_in &addr_1, const struct sockaddr_in &addr_2) const {
        return addr_1.sin_addr.s_addr == addr_2.sin_addr.s_addr;
    }
};

class server { 
private:
    // 虽然底层ip地址是一个uint32_t类型的32位整数，但我们还是习惯点分十进制字符串形式表示的IP地址
    static const char *default_ip;
    static const uint16_t default_port = 8080;
    static const size_t default_buffer_size = 1024;
public:
    server(uint16_t port = default_port, const char *ip = default_ip) : _port(port), _ip(ip) {
        lg.setScreenPrint();
    }

    void init() {
        // 要编写一个udp服务器先要创建一个网络套接字文件
        _sock_fd = socket(AF_INET, SOCK_DGRAM, 0); //AF_INET（又称PF_INET）是IPv4网络协议的套接字类型，等价于PF_INET
        if(_sock_fd < 0) {
            lg(FATAL, "socket error");
            exit(errno);
        }
        lg(INFO, "socket success");
        // 创建好套接字后要绑定一个特定的端口号，以便客户端能够找到进行通信
        // 把端口号、ip地址等相关信息先设置到本地的struct sockaddr_in变量中
        // 该结构体变量是用于存储ipv4套接字地址类型信息的
        // 再通过bind函数，设置到内核
        struct sockaddr_in addr;
        bzero(&addr, sizeof(addr));
        // 设置ipv4协议地址类型（只能设置该类型，必须和创建套接字使用的地址族相匹配）
        addr.sin_family = AF_INET; 

        // 进程间要想通信，这就要求双方得能找到对方在哪
        // 比如a给b发消息，b把消息回给a，这就说明a之前在之前就把自己的位置信息一起发给b了，所以后面b能回给a
        // 反过来也是如此，也就是说像ip地址和端口号这类数据也是需要在网络中进行传输的！！！
        // 所以要把ip地址和端口号都转换成协议所规定的网络字节序

        // 设置端口号
        // htons函数将主机字节序转换成网络字节序，即大端字节序转小端字节序
        // 必须统一成协议所规定的序列才能正确地在网络上传输
        // [0, 1023]系统内定的端口号，一般是有固定的应用层协议使用，比如http：80，https：443等
        // 所以建议绑定1024以及上的端口号，[1024, 65535]
        addr.sin_port = htons(_port);

        // 设置IP地址
        // 可以手动将字符串ip地址转成整数然后使用htonl转换成网络字节序列，也可以使用如下接口
        // inet_addr函数将点分十进制的ip地址转换成网络字节序的ip地址
        // 该函数做了两件事，把字符串ip转换成了一个整数，然后再转换成网络序列
        // addr.sin_addr.s_addr = inet_addr(_ip.c_str());  
        // 但实际上是不需要绑定特定的ip地址，因为如果一个服务器设备有多个网卡时
        // 意味着有多个IP地址，如果绑定其中一个会导致该主机只会接收到发送给这一个IP地址接收来的报文，其它客户端发送给该主机其它ip的就数据收不到了
        // 所以在绑定IP地址时通常采用的是任意地址绑定的策略，即绑定0
        // 这样无论客户端连接到服务器的哪个IP地址，服务器都能接收到这些连接请求
        addr.sin_addr.s_addr = INADDR_ANY;
        // 都是0所以转不转网络序列都无所谓
        // addr.sin_addr.s_addr = htonl(INADDR_ANY);

        // 关于本地环回地址localhost：127.0.0.1
        // 访问该地址的本质还是访问本机，用作本地cs进程间测试的地址
        // 该接口是“虚拟的”，因为它不是通过物理网络硬件（如以太网卡）与外部世界通信的。

        // 一步初始化
        // struct sockaddr_in addr = {AF_INET, htons(_port), {inet_addr(_ip.c_str())}};

        // 在本地（用户空间）设置好后，通过bind设置到内核的网络套接字文件
        // 由于这些中用于表示套接字地址的结构体类型是统一设计的，所以在传参时需要强转成通用类型struct sockaddr *
        if(bind(_sock_fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) {
            lg(FATAL, "bind error %s", strerror(errno));
            exit(errno);
        }
        lg(INFO, "bind success");
        // 创建套接字然后绑定这两步就完成了启动udp服务器
        // 后续就是收发数据了
    }

    // 接收处理数据的方法
    void run(handler_t handle) {
        // 服务器是需要一直运行的，因为在任何时间段都可能会收到请求
        // 一旦收到就得响应请求

        // 接收客户端发来的请求
        // 除了要知道请求本身的数据，也就是原始数据之外，还要知道客户端的位置信息然后保存起来
        // 方便后续响应请求时，能找到并发送给对方，如果不保存，后面就无法把响应的服务在发给对方了
        // 对于客户端也是同理

        // 定义接收数据的缓冲区和对方的（ipv4）套接字地址信息的结构体对象
        char in_buffer[default_buffer_size];
        struct sockaddr_in client_addr;
        socklen_t addrlen = sizeof(client_addr);
        while(true) {
            // 接收客户端发来的请求并保存客户端的位置信息
            ssize_t n = recvfrom(_sock_fd, in_buffer, sizeof(in_buffer) - 1, 0, (struct sockaddr *)&client_addr, &addrlen);
            if(n == -1) {
                lg(WARNING, "recvfrom error: %s", strerror(errno));
                continue;   
            }
            // 保存客户端到哈希表中，给所有客户端都响应一份
            _clients.insert(client_addr);
            // 把收到数据当作字符串看
            in_buffer[n] = '\0';
            // 打印收到的数据
            cout << "[server get a msg from " << inet_ntoa(client_addr.sin_addr) << "]$ " << in_buffer << endl;
            
            // 对数据进行简单处理然后返回给客户端
            string echo_string = handle(in_buffer);
            // 同样的，除了要响应的数据本身外，还要知道客户端的位置信息
            // 由于之前保存了客户端的地址，所以现在是知道的
            for(const auto& client : _clients) {
                // 把响应的数据发送给客户端
                sendto(_sock_fd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr *)&client, sizeof(client));
            }
        }
    }

    ~server() {
        close(_sock_fd);
        lg(INFO, "server closed");
    }

private:
    // 返回的文件描述符，所以套接字本质也是一个文件
    // 只不过对应的是网卡文件，后续操作都要用到它
    int _sock_fd;
    // 要能在网络中被定位需要有ip地址+端口号
    // 前者用来定位唯一一台主机
    // 后者用来定位这台主机中的唯一个进程
    // 实际上_ip是不需要的，采用任意地址绑定，只要端口号即可
    string _ip;
    uint16_t _port;
    unordered_set<struct sockaddr_in, sockaddr_in_hash, sockaddr_in_equal> _clients;
};

const char *server::default_ip = "0.0.0.0";
